package com.github.operationlog;

import com.github.operationlog.annotation.*;
import com.github.operationlog.mybatis.MapperMethodInterceptor;
import com.github.operationlog.output.DefaultOperationLogOutput;
import com.github.operationlog.output.OperationLogOutput;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.mybatis.spring.mapper.MapperFactoryBean;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.util.ClassUtils;

import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

/**
 * Operation Log {@link BeanPostProcessor}
 *
 * @author luke.luo
 */
@Slf4j
public class OperationLogAnnotationBeanPostProcessor implements BeanPostProcessor {

    /**
     * 操作日志{@link org.springframework.cglib.proxy.MethodInterceptor}表。
     * key为beanName，value为{@link OperationLogMethodInterceptor}
     */
    private Map<String, OperationLogMethodInterceptor> operationLogMethodInterceptorMap = new ConcurrentHashMap<>();

    /**
     * 产生操作日志的BeanClass，key为beanName，value为BeanClass
     */
    private Map<String, Class<?>> operationLogBeanClassMap = new ConcurrentHashMap<>();

    /**
     * 日志数据输出对象
     */
    private OperationLogOutput operationLogOutput = new DefaultOperationLogOutput();

    // 默认需要记录操作日志的方法
    private static final List<String> DEFAULT_ADDITION_METHOD_NAME = Arrays.asList("insert", "create", "createSelective", "save");
    private static final List<String> DEFAULT_UPDATE_METHOD_NAME = Arrays.asList("update", "updateWithNull", "modify");
    private static final List<String> DEFAULT_DELETE_METHOD_NAME = Arrays.asList("delete", "remove");
    private static final List<String> DEFAULT_SELECT_METHOD_NAME = Arrays.asList("selectById", "load", "findById");

    public OperationLogAnnotationBeanPostProcessor() {}

    public void setOutput(OperationLogOutput operationLogOutput) {
        this.operationLogOutput = operationLogOutput;
    }

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        if (Objects.isNull(bean)) {
            return null;
        }

        // 业务操作BeanClass
        Class<?> beanClass = resolveBeanClass(bean);

        OperationLog operationLogAnnotation = beanClass.getAnnotation(OperationLog.class);
        if (Objects.isNull(operationLogAnnotation)) {
            // 该Bean未开启日志功能（必须配置OperationLog才能记录日志）
            return bean;
        }

        OperationLogMethodInterceptor operationLogMethodInterceptor = new OperationLogMethodInterceptor(
                operationLogAnnotation.table(), this.operationLogOutput);

        for (Method method : beanClass.getMethods()) {
            OperationLogMethod operationLogMethodAnnotation = method.getAnnotation(OperationLogMethod.class);
            if (Objects.isNull(operationLogMethodAnnotation)) {
                continue;
            }

            MethodAttribute methodAttribute = buildMethodAttribute(
                    method, operationLogAnnotation, operationLogMethodAnnotation
            );

            methodAttribute.setTargetName(beanName);
            operationLogMethodInterceptor.addMethodAttribute(methodAttribute);
        }

        for (OperationLogMethod operationLogMethodAnnotation : operationLogAnnotation.methods()) {
            Method method = resolveMethod(operationLogMethodAnnotation, beanClass.getMethods());
            if (method == null) {
                continue;
            }

            MethodAttribute methodAttribute = buildMethodAttribute(
                    method, operationLogAnnotation, operationLogMethodAnnotation
            );

            methodAttribute.setTargetName(beanName);
            operationLogMethodInterceptor.addMethodAttribute(methodAttribute);
        }

        // 默认操作方法
        for (Method method : beanClass.getMethods()) {
            MethodAttribute methodAttribute = buildDefaultMethodAttribute(method, operationLogAnnotation);
            if (methodAttribute != null) {
                methodAttribute.setTargetName(beanName);
                operationLogMethodInterceptor.addMethodAttribute(methodAttribute);
            }
        }

        // 根据ID查询实体的方法
        Method findByIdMethod = resolveMethod(beanClass.getMethods(), DEFAULT_SELECT_METHOD_NAME);
        operationLogMethodInterceptor.setFindByIdMethod(findByIdMethod);

        operationLogMethodInterceptorMap.put(beanName, operationLogMethodInterceptor);
        operationLogBeanClassMap.put(beanName, beanClass);
        OperationLogListable.register(operationLogAnnotation.table(), operationLogAnnotation.name());
        return bean;
    }


    /**
     * 构建操作日志方法属性{@link MethodAttribute}
     *
     * @param method 操作方法
     * @param operationLogAnnotation OperationLog注解
     * @param operationLogMethodAnnotation OperationLogMethod 注解
     * @return {@link MethodAttribute}实例
     * @see MethodAttribute
     */
    private MethodAttribute buildMethodAttribute(
            Method method, OperationLog operationLogAnnotation,
            OperationLogMethod operationLogMethodAnnotation)
    {
        MethodAttribute methodAttribute = MethodAttribute.builder()
                .method(method)
                .entityParam(true)
                .type(operationLogMethodAnnotation.type())
                .entityClass(operationLogAnnotation.type())
                .businessName(operationLogAnnotation.name())
                .entityFieldList(parseEntityField(operationLogAnnotation.type()))
                .build();

        if (StringUtils.isNotBlank(operationLogMethodAnnotation.note())) {
            String businessName = methodAttribute.getBusinessName() + " - " + operationLogMethodAnnotation.note();
            methodAttribute.setBusinessName(businessName);
        }

        OperationLogPrimaryKey operationLogPrimaryKeyAnnotation = method.getAnnotation(OperationLogPrimaryKey.class);
        if (operationLogPrimaryKeyAnnotation != null) {
            methodAttribute.setEntityParam(false);
            methodAttribute.setPrimaryKeyPosition(operationLogPrimaryKeyAnnotation.position());
        }

        return methodAttribute;
    }

    /**
     * 构建默认方法操作日志方法属性{@link MethodAttribute}
     *
     * @param method 操作方法
     * @param operationLogAnnotation OperationLog注解
     * @return {@link MethodAttribute}实例
     * @see MethodAttribute
     */
    private MethodAttribute buildDefaultMethodAttribute(Method method, OperationLog operationLogAnnotation) {
        OperationType operationType;
        if (DEFAULT_ADDITION_METHOD_NAME.contains(method.getName())) {
            operationType = OperationType.INSERT;
        } else if (DEFAULT_UPDATE_METHOD_NAME.contains(method.getName())) {
            operationType = OperationType.UPDATE;
        } else if (DEFAULT_DELETE_METHOD_NAME.contains(method.getName())) {
            operationType = OperationType.DELETE;
        } else {
            return null;
        }

        return MethodAttribute.builder()
                .method(method)
                .entityParam(true)
                .type(operationType)
                .entityClass(operationLogAnnotation.type())
                .businessName(operationLogAnnotation.name())
                .entityFieldList(parseEntityField(operationLogAnnotation.type()))
                .build();
    }

    /**
     * 解析并构建实体字段，当这些字段值发生改变，将记录操作日志
     *
     * @param entityClass 实体类型
     * @return 实体字段列表
     */
    private List<EntityField> parseEntityField(Class<?> entityClass) {
        return Arrays.stream(entityClass.getDeclaredFields())
                .filter(field -> {
                    OperationLogIgnore operationLogIgnore = field.getAnnotation(OperationLogIgnore.class);
                    return Objects.isNull(operationLogIgnore) || !operationLogIgnore.ignore();
                })
                .map(field ->  {
                    OperationLogField operationLogFieldAnnotation = field.getAnnotation(OperationLogField.class);
                    if (operationLogFieldAnnotation != null) {
                        return new EntityField(
                                field.getName(),
                                operationLogFieldAnnotation.name(),
                                "id".equalsIgnoreCase(field.getName())
                        );
                    }

                    return new EntityField(
                            field.getName(),
                            "id".equalsIgnoreCase(field.getName())
                    );
                })
                .collect(Collectors.toList());
    }

    /**
     * 解析BeanClass
     *
     * @param bean bean
     * @return BeanClass
     */
    private Class<?> resolveBeanClass(Object bean) {
        if (bean instanceof MapperFactoryBean) {
            return ((MapperFactoryBean) bean).getMapperInterface();
        }
        return ClassUtils.isCglibProxyClass(bean.getClass()) ? bean.getClass().getSuperclass() : bean.getClass();
    }

    private Method resolveMethod(OperationLogMethod operationLogMethodAnnotation, Method[] methodList) {
        return StringUtils.isNotBlank(operationLogMethodAnnotation.name()) ?
                resolveMethod(methodList, Collections.singletonList(operationLogMethodAnnotation.name())) :
                null;
    }

    private Method resolveMethod(Method[] methodList, List<String> methodNameList) {
        for (String methodName : methodNameList) {
            for (Method method : methodList) {
                if (methodName.equals(method.getName())) {
                    return method;
                }
            }
        }
        return null;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (!operationLogMethodInterceptorMap.containsKey(beanName) || !operationLogBeanClassMap.containsKey(beanName)) {
            return bean;
        }

        if (bean instanceof MapperFactoryBean) {

            /*
             * 创建MapperFactoryBean实例代理。
             * 通过给MapperFactoryBean做代理，当调用其getObject()方法时，将返回一个Mapper代理的代理对象，
             * 代理对象的处理回调实例为OperationLogMethodInterceptor，其target（方法实际的调用对象）即为
             * Mapper代理对象，从而实现对Mapper接口的代理
             */

            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(MapperFactoryBean.class);

            MapperMethodInterceptor mapperMethodInterceptor = new MapperMethodInterceptor(
                    (MapperFactoryBean) bean, operationLogMethodInterceptorMap.get(beanName));
            enhancer.setCallback(mapperMethodInterceptor);

            operationLogBeanClassMap.remove(beanName);
            operationLogMethodInterceptorMap.remove(beanName);

            return enhancer.create();
        }

        // 创建Bean的操作日志代理
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(operationLogBeanClassMap.get(beanName));

        OperationLogMethodInterceptor operationLogMethodInterceptor = operationLogMethodInterceptorMap.get(beanName);
        operationLogMethodInterceptor.setTarget(bean);
        operationLogMethodInterceptor.afterPropertiesSet();

        enhancer.setCallback(operationLogMethodInterceptor);

        operationLogBeanClassMap.remove(beanName);
        operationLogMethodInterceptorMap.remove(beanName);

        return enhancer.create();
    }
}
